#include <kern/monitor.h>
#include <kern/memlayout.h>
#include <x86.h>

int cursor_y = 0;
int cursor_x = 0;
uint16_t *video_memory = (uint16_t*)(KERNBASE + 0xB8000);

void monitor_adjust_to_phys() {
	video_memory = (uint16_t*)0xB8000;
}

// Updates the hardware cursor.
static void move_cursor()
{
	// The screen is 80 characters wide...
	uint16_t cursorLocation = cursor_y * 80 + cursor_x;
	outb(0x3D4, 14);                  // Tell the VGA board we are setting the high cursor byte.
	outb(0x3D5, cursorLocation >> 8); // Send the high cursor byte.
	outb(0x3D4, 15);                  // Tell the VGA board we are setting the low cursor byte.
	outb(0x3D5, cursorLocation);      // Send the low cursor byte.
}

// Scrolls the text on the screen up by one line.
static void scroll()
{
	// Get a space character with the default colour attributes.
	uint8_t attributeByte = (0 /*black*/ << 4) | (15 /*white*/ & 0x0F);
	uint16_t blank = 0x20 /* space */ | (attributeByte << 8);

	// Row 25 is the end, this means we need to scroll up
	if(cursor_y >= 25) {
		// Move the current text chunk that makes up the screen
		// back in the buffer by a line
		int i;
		for (i=0; i < 24*80; i++) {
			video_memory[i] = video_memory[i+80];
		}

		// The last line should now be blank. Do this by writing
		// 80 spaces to it.
		for (i=24*80; i < 25*80; i++) {
			video_memory[i] = blank;
		}
		// The cursor should now be on the last line.
		cursor_y = 24;
	}
}

// Writes a single character out to the screen.
static void monitor_put(char c)
{
	// The background colour is black (0), the foreground is white (15).
	uint8_t backColour = 0;
	uint8_t foreColour = 15;

	// The attribute byte is made up of two nibbles - the lower being the
	// foreground colour, and the upper the background colour.
	uint8_t  attributeByte = (backColour << 4) | (foreColour & 0x0F);
	// The attribute byte is the top 8 bits of the word we have to send to the
	// VGA board.
	uint16_t attribute = attributeByte << 8;
	uint16_t *location;

	// Handle a backspace, by moving the cursor back one space
	if (c == 0x08 && cursor_x) {
		cursor_x--;
	}

	switch (c) {
	case '\n':
		++cursor_y;
		/* fallthrough */
	case '\r':
		cursor_x = 0;
		break;
	case 0x09:
		cursor_x = (cursor_x+8) & ~(8-1); // tab
		break;
	default:
		location = video_memory + (cursor_y*80 + cursor_x);
		*location = c | attribute;
		++cursor_x;
	}

	// Check if we need to insert a new line because we have reached the end
	// of the screen.
	if (cursor_x >= 80) {
		cursor_x = 0;
		cursor_y ++;
	}

	// Scroll the screen if needed.
	scroll();
	// Move the hardware cursor.
	move_cursor();
}

// Clears the screen, by copying lots of spaces to the framebuffer.
void monitor_clear()
{
	// Make an attribute byte for the default colours
	uint8_t attributeByte = (0 /*black*/ << 4) | (15 /*white*/ & 0x0F);
	uint16_t blank = 0x20 /* space */ | (attributeByte << 8);

	int i;
	for (i = 0; i < 80*25; i++) {
		video_memory[i] = blank;
	}

	// Move the hardware cursor back to the start.
	cursor_x = 0;
	cursor_y = 0;
	move_cursor();
}

void monitor_write(char *c)
{
	for (;*c;++c)
		monitor_put(*c);
}

void monitor_write_hex(uint32_t n)
{
	monitor_write("0x");
	monitor_write_num(n, 16);
}

void monitor_write_dec(uint32_t n)
{
	monitor_write_num(n, 10);
}

void monitor_write_num(uint32_t n, unsigned int base)
{
	char buff[11];
	char tmp;
	int i, j, d;
	
	if (!n) {
		monitor_write("0");
		return;
	}

	for (i=0; n; ++i) {
		d = n % base;
		buff[i] = d < 10 ? '0'+d : 'A'+(d-10);
		n /= base;
	}
	buff[i] = '\0';
	
	for (j=0, i=i-1; i > j; ++j, --i) {
		tmp = buff[j];
		buff[j] = buff[i];
		buff[i] = tmp;
	}

	monitor_write(buff);
}



